﻿/*
 * Copyright: This software is made available as-is without limitation or warranty.
 * 
 * Source Control Header Information
 * $Id: $
 * $Author: $
 * $Date: $
 * */
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace NSRT_Console
{
	class Program
	{
		#region Structures
		/// <summary>
		/// Labview LStrHandle type
		/// </summary>
		[StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
		public struct LStrHandle
		{
			/// <summary>
			/// No of bytes to follow
			/// </summary>
			public Int32 Length;
			/// <summary>
			/// String array of length no_of_bytes
			/// </summary>
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst=8192)]
			public string Value;
		}
		#endregion
		#region Enumerations
		/// <summary>
		/// Filter weighting
		/// </summary>
		public enum FS_WEIGHTING : byte
		{
			A_WEIGHTED = 1,
			C_WEIGHTED = 2
		}
		#endregion
		#region External Invokes to the DLL
		/// <summary>
		/// DLL Status
		/// </summary>
		/// <param name="errStr"></param>
		/// <param name="errStrLen"></param>
		/// <param name="module"></param>
		/// <returns></returns>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 LVDLLStatus([MarshalAs(UnmanagedType.LPStr),Out] out string errStr, [MarshalAs(UnmanagedType.I4),In,Out] ref int errStrLen, IntPtr module);
		/// <summary>
		/// 
		/// </summary>
		/// <param name="board_number">Board number (usually 0 will work)</param>
		/// <param name="start_time">No of seconds since 1/1/1904 00:00:00</param>
		/// <returns></returns>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Arm_Timer(ref Int32 board_number, UInt64 start_time);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Cancel_Timer(ref Int32 board_number);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Initialize(ref Int32 board_number, Int16 reset);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Close(ref Int32 board_number);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_Batt(ref Int32 board_number, [MarshalAs(UnmanagedType.R8), Out] out double batt_volts);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_Levels(ref Int32 board_number, [MarshalAs(UnmanagedType.R8), Out] out double level, [MarshalAs(UnmanagedType.U1), Out] out byte weighting);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_LogMemory(ref Int32 board_number, ref IntPtr data);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_RTBuffer(ref Int32 board_number, [MarshalAs(UnmanagedType.U2)] UInt16 no_samples, [MarshalAs(UnmanagedType.U4)] UInt32 timeout_ms, ref IntPtr data);
		/// <summary>
		/// Read the filter settings
		/// </summary>
		/// <param name="board_number"></param>
		/// <param name="fs"></param>
		/// <param name="weighting"></param>
		/// <param name="time_const"></param>
		/// <returns></returns>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_Fs_Filters(ref Int32 board_number, [MarshalAs(UnmanagedType.R8), Out] out double fs, [MarshalAs(UnmanagedType.U1), Out] out byte weighting, [MarshalAs(UnmanagedType.R8), Out] out double time_const);
		/// <summary>
		/// Write the filter settings
		/// </summary>
		/// <param name="board_number"></param>
		/// <param name="weighting"></param>
		/// <param name="time_const"></param>
		/// <returns></returns>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Write_Fs_Filters(ref Int32 board_number, [MarshalAs(UnmanagedType.U1), In] byte weighting, [MarshalAs(UnmanagedType.R8), In] double time_const);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_Manifest(ref Int32 board_number, [Out] out UInt16 manifest);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Start_Record(ref Int32 board_number);
		/// <summary>
		/// Read instrument information
		/// </summary>
		/// <param name="board_number"></param>
		/// <param name="user_id"></param>
		/// <param name="model"></param>
		/// <param name="revision"></param>
		/// <param name="dob"></param>
		/// <param name="last_calibration"></param>
		/// <param name="serial_no"></param>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern void NSRT_LVD_Read_InstInfo(ref Int32 board_number, 
			ref IntPtr user_id,
			ref IntPtr model,
			ref IntPtr revision,
			ref IntPtr dob,
			ref IntPtr last_calibration,
			ref IntPtr serial_no);
		/// <summary>
		/// Read the current temperature of the onboard thermometer
		/// </summary>
		/// <param name="board_number"></param>
		/// <param name="temp_in_degc"></param>
		/// <returns></returns>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_Temp(ref Int32 board_number, [MarshalAs(UnmanagedType.R8), Out] out double temp_in_degc);
		/// <summary>
		/// Read the system clock
		/// </summary>
		/// <param name="board_number"></param>
		/// <param name="utc_time"></param>
		/// <returns></returns>
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Read_Time(ref Int32 board_number, [MarshalAs(UnmanagedType.U8), Out] out UInt64 utc_time);
		[DllImport(@"NSRT_LVD_DLL.dll", CallingConvention = CallingConvention.Cdecl)]
		private static extern Int32 NSRT_LVD_Write_Time(ref Int32 board_number, [MarshalAs(UnmanagedType.U8)] UInt64 utc_time);
		#endregion
		
		/// <summary>
		/// Reverses the byte order of double so as to reflect big endian encoded data
		/// </summary>
		/// <param name="little_endian"></param>
		/// <returns></returns>
		static double BigEndianDouble(double little_endian)
		{
			byte[] big_endian = BitConverter.GetBytes(little_endian);
			Array.Reverse(big_endian);
			return BitConverter.ToDouble(big_endian, 0);
		}
		/// <summary>
		/// Allows little to big endian conversion of a byte array at a position specified
		/// </summary>
		/// <param name="array"></param>
		/// <param name="start"></param>
		/// <returns></returns>
		static double BigEndianDouble(byte[] array, int start)
		{
			if (!BitConverter.IsLittleEndian)
				return BitConverter.ToDouble(array, start);
			double little_endian = BitConverter.ToDouble(array, start);
			return BigEndianDouble(little_endian);
		}
		/// <summary>
		/// Converts to a big endian 4 byte (32 bit) integer
		/// </summary>
		/// <param name="array"></param>
		/// <param name="start"></param>
		/// <returns></returns>
		static int BigEndianInt32(byte[] array, int start)
		{
			if (!BitConverter.IsLittleEndian)
				return BitConverter.ToInt32(array, start);
			int little_endian = BitConverter.ToInt32(array, start);
			byte[] big_endian = BitConverter.GetBytes(little_endian);
			Array.Reverse(big_endian);
			return BitConverter.ToInt32(big_endian, 0);
		}
		/// <summary>
		/// Converts to a big endian 8 byte (64 bit) unsigned integer
		/// </summary>
		/// <param name="array"></param>
		/// <param name="start"></param>
		/// <returns></returns>
		static ulong BigEndianUInt64(byte[] array, int start)
		{
			if (!BitConverter.IsLittleEndian)
				return BitConverter.ToUInt64(array, start);
			ulong little_endian = BitConverter.ToUInt64(array, start);
			byte[] big_endian = BitConverter.GetBytes(little_endian);
			Array.Reverse(big_endian);
			return BitConverter.ToUInt64(big_endian, 0);
		}
		static long BigEndianInt64(byte[] array, int start)
		{
			if (!BitConverter.IsLittleEndian)
				return BitConverter.ToInt64(array, start);
			long little_endian = BitConverter.ToInt64(array, start);
			byte[] big_endian = BitConverter.GetBytes(little_endian);
			Array.Reverse(big_endian);
			return BitConverter.ToInt64(big_endian, 0);
		}
		/// <summary>
		/// Fetches a string encoded data handle
		/// <para>Note: The DLL returns a pointer to a pointer. So we need to extract the embedded pointer first.</para>
		/// </summary>
		/// <param name="ptr">Pointer value returned by the DLL call</param>
		/// <returns></returns>
		private static string FetchLVStrHandle(IntPtr ptr, Encoding encoding = null)
		{
			if (ptr == IntPtr.Zero) return null;	// error
			byte[] len = new byte[4];	// this is just used to store int32 length values
			Marshal.Copy(ptr, len, 0, 4);		// this will get us the length of the pointer (which should be 4 bytes ie. 32 bits)
			IntPtr ptr_to_data = (IntPtr)BitConverter.ToInt32(len, 0);	// this is a pointer contained in our pointer
			Marshal.Copy(ptr_to_data, len, 0, 4);	// reuse the len byte array to avoid redeclaring it.
			int data_length = BitConverter.ToInt32(len, 0);		// how long is the actual string at this address
			byte[] data = new byte[data_length + 4];	// create a data buffer to store the actual string (including the 4 byte length prefix)
			Marshal.Copy(ptr_to_data, data, 0, data_length + 4);	// the data is now contained in a managed byte array
			// Now, unmanaged marshalling might leak; zeroing the pointer will remove the reference and allow the GC to collect any managed memory used in the copy.
			ptr_to_data = IntPtr.Zero;	// probably a bad idea to FreeHAlloc on this pointer because that will remove the unmanaged data
			if (encoding == null)	// not specified so use ASCII
				encoding = new ASCIIEncoding();
			string lv_str = encoding.GetString(data, 4, data_length);	// finally, convert to string.
			// Null a few items to assist the GC in clean up
			data = null; len = null;
			return lv_str;
		}
		/// <summary>
		/// An alias to the external reference to tidy up the function call
		/// </summary>
		/// <param name="?"></param>
		/// <returns></returns>
		static Dictionary<string, string> NSRT_LV_Read_InstInfo(ref int board_no)
		{
			Dictionary<string, string> inst_info = new Dictionary<string, string>();
			IntPtr sn = IntPtr.Zero, model = IntPtr.Zero, user_id = IntPtr.Zero, dob = IntPtr.Zero, version = IntPtr.Zero, last_cal = IntPtr.Zero;
			NSRT_LVD_Read_InstInfo(ref board_no, ref user_id, ref model, ref version, ref dob, ref last_cal, ref sn);
			if (model == IntPtr.Zero) throw new Exception("Device not found.");
			string[] keys = new string[] { "model", "sn", "user_id", "dob", "last_cal", "version" };
			foreach (string key in keys)
			{
				string a = "";
				switch (key)
				{
					case "model":
						a = FetchLVStrHandle(model);
						break;
					case "sn":
						a = FetchLVStrHandle(sn);
						break;
					case "user_id":
						a = FetchLVStrHandle(user_id);
						break;
					case "dob":
						a = FetchLVStrHandle(dob);
						break;
					case "version":
						a = FetchLVStrHandle(version);
						break;
					case "last_cal":
						a = FetchLVStrHandle(last_cal);
						break;
				}
				if (a != null)
					inst_info.Add(key, a);	// null indicates an error so only add those with data (empty string is valid)
			}	
			return inst_info;	// finally return the dictionary
		}

		/// <summary>
		/// Main arguments
		/// </summary>
		/// <param name="args"></param>
		static void Main(string[] args)
		{
			Console.WriteLine("Attempting to reference NSRT_LVD_DLL.dll");
			string error = "";
			int err_len = 0;
			int board_no = 0;
			Int16 reset = 0;
			double batt_voltage = 0, temp = 0, level = 0, fs = 0, time_const = 0;

			//LStrHandle l = new LStrHandle();
			//IntPtr pnt = Marshal.AllocHGlobal(Marshal.SizeOf(l));
			IntPtr module = IntPtr.Zero;
			byte weighting = 0;
			int msg = LVDLLStatus(out error, ref err_len, module);	// get the status of the dll as a starter for 10
			Console.WriteLine("LVDLLStatus - Res: {0}. Error String: {1}. Len: {2}. Module Ptr: {3}", msg, error, err_len, module);
			int res = NSRT_LVD_Initialize(ref board_no, reset);
			Console.WriteLine("NSRT_LVD_Initialize - Res: {0}", res);
			res = NSRT_LVD_Read_Fs_Filters(ref board_no, out fs, out weighting, out time_const);
			Console.WriteLine("NSRT_LVD_Read_Fs_Filters = Res: {0}. Fs: {1:0.000}. Weighting: {2}.  Time Constant: {3:0.0000}", res, fs, weighting, time_const);
			Dictionary<string, string> inst_info = NSRT_LV_Read_InstInfo(ref board_no);

			// These pointers can now be used to marshal data across
			IntPtr ptr_to_logptr = IntPtr.Zero;
			res = NSRT_LVD_Read_LogMemory(ref board_no, ref ptr_to_logptr);
			byte[] byte_len = new byte[4];
			Marshal.Copy(ptr_to_logptr, byte_len, 0, 4);
			IntPtr ptr_to_data = (IntPtr)BitConverter.ToInt32(byte_len, 0);
			Marshal.Copy(ptr_to_data, byte_len, 0, 4);
			Int32 data_length = BitConverter.ToInt32(byte_len, 0);
			byte[] data = new byte[data_length + 4];
			Marshal.Copy(ptr_to_data, data, 0, data_length + 4);	// perform the copy from memory
			// b now should contain all the data downloaded
			// We currently have 4 records
			List<double> lmax = new List<double>();
			List<double> leq = new List<double>();
			List<double> lmin = new List<double>();
			Int32 total_length = BitConverter.ToInt32(data, 0);	// 0-3
			int ptr_offset = 4;	// data starts at pos. 4
			Int32 no_records = BigEndianInt32(data, ptr_offset);	// 4-7
			ptr_offset += 4;// This bit is the record header
			DateTime dt; double integration_time; int no_of_intervals;
			lmax = ReadDataBlock(data, ref ptr_offset, out dt, out integration_time, out no_of_intervals);
			leq = ReadDataBlock(data, ref ptr_offset, out dt, out integration_time, out no_of_intervals);
			lmin = ReadDataBlock(data, ref ptr_offset, out dt, out integration_time, out no_of_intervals);
			//ptr_offset += 4;
			UInt64 utc = BigEndianUInt64(data, ptr_offset);
			DateTime dt1 = new DateTime(1904, 1, 1, 0, 0, 0);
			dt1 = dt1.AddSeconds((double)utc);
			ptr_offset += 8;
			double interval = BigEndianDouble(data, ptr_offset);
			ptr_offset += 8;
			double sampling_frequency = BigEndianDouble(data, ptr_offset);
			ptr_offset += 8;
			//double something = BigEndianDouble(data, ptr_offset);
			//ptr_offset += 8;
			byte b_weighting = data[ptr_offset++];
			byte b_lmax = data[ptr_offset++];
			byte b_leq = data[ptr_offset++];
			byte b_lmin = data[ptr_offset++];
			// At this point we should be at the end of the record
			do
			{
				IntPtr data_ptr = IntPtr.Zero, sub_ptr = IntPtr.Zero;
				byte[] sub_addr = new byte[4];
				while (!Console.KeyAvailable)
				{
					res = NSRT_LVD_Read_Batt(ref board_no, out batt_voltage);
					res = NSRT_LVD_Read_Temp(ref board_no, out temp);
					res = NSRT_LVD_Read_Levels(ref board_no, out level, out weighting);
					//res = NSRT_LVD_Read_RTBuffer(ref board_no, 2000, 100, ref data_ptr);
					//Marshal.Copy(data_ptr, sub_addr, 0, 4);
					//sub_ptr = (IntPtr)BitConverter.ToInt32(sub_addr, 0);
					//Marshal.Copy(sub_ptr, sub_addr, 0, 4);
					//Int32 data_len = BitConverter.ToInt32(sub_addr, 0);
					//byte[] data_arr = new byte[data_len + 4];
					//Marshal.Copy(sub_ptr, data_arr, 0, data_len + 4);
					Console.WriteLine("Voltage: {0:0.00}. Temp: {1:0.00}. SPL: {2:0.00}.", new object[] { batt_voltage, temp, level });
					System.Threading.Thread.Sleep(125);	// sleep a moment and repeat
				}

			} while (Console.ReadKey(true).Key != ConsoleKey.Escape);
			res = NSRT_LVD_Close(ref board_no);
			Console.WriteLine("NSRT_LVD_Close - Res: {0}", res);
			Console.WriteLine("Press any key to continue...");
			Console.ReadKey();

		}
		/// <summary>
		/// Reads a data record
		/// </summary>
		/// <param name="data"></param>
		/// <param name="ptr_offset"></param>
		/// <returns></returns>
		private static List<double> ReadDataBlock(byte[] data, ref int ptr_offset, out DateTime dt, out double interval, out int no_of_intervals)
		{
			dt = DateTime.MinValue; interval = 0; no_of_intervals = 0;
			// If this is not manifested it will be empty
			// TODO: allow this to handle empty data
			double origin = BigEndianDouble(data, ptr_offset);	// 8-15
			ptr_offset += 8;
			DateTime dt_origin = new DateTime(1904, 1, 1, 0, 0, 0);
			dt_origin = dt_origin.AddSeconds(origin);
			dt = dt_origin;
			double scale = BigEndianDouble(data, ptr_offset);	// 16-23
			ptr_offset += 8;
			interval = scale;
			Int32 no_intervals = BigEndianInt32(data, ptr_offset);	// 24-27
			ptr_offset += 4;
			no_of_intervals = no_intervals;
			// We now know how many intervals we'll have;
			int block_size = 8 * no_intervals;
			List<double> data1 = new List<double>(no_intervals);
			for (int ii = 0; ii < block_size; ii += 8)
			{
				double val = BigEndianDouble(data, ii + ptr_offset);
				data1.Add(val);
			}
			ptr_offset += block_size;
			return data1;
		}

	}
}
